<pre class="metadata">
Title: WebUSB Testing API
Status: ED
ED: https://wicg.github.io/webusb/test/
Shortname: webusb-test
Level: 1
Editor: Reilly Grant 83788, Google LLC https://www.google.com, reillyg@google.com
Editor: Ovidio Henriquez 106543, Google LLC https://www.google.com, odejesush@google.com
Abstract: This document describes an API for testing a User Agent's implementation of the WebUSB API.
Group: wicg
Repository: https://github.com/WICG/webusb/
!Participate: <a href="https://www.w3.org/community/wicg/">Join the W3C Community Group</a>
!Participate: <a href="irc://irc.w3.org:6665/#webusb">IRC: #webusb on W3C's IRC</a> (Stay around for an answer, it make take a while)
</pre>

# Introduction # {#intro}

<em>This section is non-normative</em>.

Standards such as [[WebUSB]] pose a challenge to test authors because to fully
exercise their interfaces requires physical hardware devices that respond in
predictable ways. To address this challenge this specification defines an
interface for controlling a simulation of the USB subsystem on the host the
User Agent (UA) is running on. With this interface devices with particular properties
can be created and their responses to requests are well defined.

The purpose if this interface is to assist the developers of UAs and so this
interface does not permit arbitrary control over the behavior of devices. Some
parameters are configurable and some are specified.

The only intended client of this API are tests in
<a href="https://github.com/web-platform-tests/wpt">Web Platform Tests</a>.
This testing
API will be changed as needed to support the evolution of those tests.

Testing applications that use WebUSB is specifically not in scope
for this API. It not designed for that purpose. And, the ability
to make breaking changes to this API has higher priority.


## Examples ## {#examples}

<div class="example">
This example, expressed as a W3C testharness.js-based test, demonstrates
initializing this API and using it to verify that devices can be added and
removed.

<pre>
  let fakeDeviceInit = {
    usbVersionMajor: 2,
    usbVersionMinor: 0,
    usbVersionSubminor: 0,
    deviceClass: 0xFF,
    deviceSubclass: 0xFF,
    deviceProtocol: 0xFF,
    vendorId: 0x1234,
    productId: 0xABCD,
    deviceVersionMajor: 1,
    deviceVersionMinor: 0,
    deviceVersionSubminor: 0,
    configurations: []
  };

  function promiseForEvent(eventTarget, eventType) {
    return new Promise(resolve => {
      let eventHandler = evt => {
        resolve(evt);
        eventTarget.removeEventListener(eventTarget, eventHandler);
      };
      eventTarget.addEventListener(eventType);
    });
  }

  promise_test(async () => {
    await navigator.usb.test.initialize();

    let fakeDevice = navigator.usb.addFakeDevice(fakeDeviceInit);
    let connectEvent = await promiseForEvent(navigator.usb, 'connect');
    let device = connectEvent.device;
    assert_equals(device.usbVersionMajor, fakeDeviceInit.usbVersionMajor);
    assert_equals(device.usbVersionMinor, fakeDeviceInit.usbVersionMinor);
    assert_equals(device.usbVersionSubminor, fakeDeviceInit.usbVersionSubminor);
    assert_equals(device.deviceClass, fakeDeviceInit.deviceClass);
    assert_equals(device.deviceSubclass, fakeDeviceInit.deviceSubclass);
    assert_equals(device.deviceProtocol, fakeDeviceInit.deviceProtocol);
    assert_equals(device.vendorId, fakeDeviceInit.vendorId);
    assert_equals(device.productId, fakeDeviceInit.productId);
    assert_equals(device.deviceVersionMajor, fakeDeviceInit.deviceVersionMajor);
    assert_equals(device.deviceVersionMinor, fakeDeviceInit.deviceVersionMinor);
    assert_equals(device.deviceVersionSubminor, fakeDeviceInit.deviceVersionSubminor);
    assert_equals(device.configuration, null);
    assert_equals(device.configurations.length, 0);

    let devices = await navigator.usb.getDevices();
    assert_array_equals(devices, [device]);

    fakeDevice.disconnect();
    let disconnectEvent = await promiseForEvent(navigator.usb, 'disconnect');
    assert_equals(disconnectEvent.device, device);
  });
</pre>
</div>

<div class="example">
This example, which reuses definitions from the previous example and is also
expressed as a W3C testharness.js-based test, demonstrates a test of the
<code>navigator.usb.requestDevice()</code> method.

<pre>
  promise_test(async () => {
    await navigator.usb.test.initialize();
    let fakeDevice = navigator.usb.addFakeDevice(fakeDeviceInit);
    let connectEvent = await promiseForEvent(navigator.usb, 'connect');
    navigator.usb.test.onrequestdevice = () => connectEvent.device;

    let options = { filters: [{ vendorId: 0x1234 }] };
    let device = await navigator.usb.requestDevice(options);
    assert_array_equals(navigator.usb.test.lastFilters, options.filters);
    assert_equals(device, connectEvent.device);
  }
</pre>
</div>

Note: If multiple tests are run in the same document then the test harness
should invoke {{USBTest/reset()}} and wait until the {{Promise}} it returns is
resolved before executing the next test.

# Availability # {#availability}

This specification defines an interface that is not intended be used by
non-testing-related web content. The UA MAY choose to expose this interface
only when a runtime or compile-time flag has been set.

<div class="note">
Note, as an example, in the Chromium Project this specification is implemented
by a JavaScript polyfill on top of a lower-level interface. This interface is
only available in a binary explicitly built for running web platform test cases.
A runtime flag is also necessary to enable this testing mode. This design has
two benefits,

1.  The risk of introducing a security vulnerability in the default
    configuration is mitigated.
1.  The polyfill is not shipped with the production application and so there is
    no increase in binary size to support an API that will rarely be used.

</div>

# Global Testing Interface # {#test-interface}

<xmp class="idl">
  partial interface USB {
    [SameObject] readonly attribute USBTest test;
  };

  interface USBDeviceRequestEvent : Event {
    attribute FrozenArray<USBDeviceFilter> filters;

    void respondWith(Promise<FakeUSBDevice> result);
  };

  interface USBTest {
    attribute EventHandler onrequestdevice;

    Promise<void> initialize();
    Promise<void> attachToContext((HTMLIFrameElement or Worker) context);
    FakeUSBDevice addFakeDevice(FakeUSBDeviceInit deviceInit);
    Promise<void> reset();
  };
</xmp>

By default, a UA SHALL NOT alter the behavior of a {{USB}} instance |usb| in any
<a>global object</a> until it is reconfigured so that |usb| is <dfn>controlled
by</dfn> a {{USBTest}} instance |test|. At that point the behavior of |usb| is
defined by this specification.

Instances of {{USBTest}} are created with an <a>internal slot</a>
<dfn attribute for="USBTest">\[[initializationPromise]]</dfn> with an initial
value of <code>null</code>.

When invoked, the {{USBTest/initialize()}} method MUST run these steps:

1.  Let |test| be the {{USBTest}} instance on which this method was invoked.
1.  If |test|.{{USBTest/[[initializationPromise]]}} is <code>null</code> then
    set |test|.{{USBTest/[[initializationPromise]]}} to a new {{Promise}} and
    run these sub-steps <a>in parallel</a>:
    1.  Reconfigure the UA's internal implementation of the {{Navigator/usb}}
        object in the <a>current global object</a> so that it is <a>controlled
        by</a> |test|.
    1.  <a>Resolve</a> |test|.{{USBTest/[[initializationPromise]]}}.
1.  Return |test|.{{USBTest/[[initializationPromise]]}}.

When invoked, the {{USBTest/attachToContext(context)}} method MUST return a new
{{Promise}} |promise| and run these steps <a>in parallel</a>:

1.  Let |test| be the {{USBTest}} instance on which this method was invoked.
1.  If |test|.{{USBTest/[[initializationPromise]]}} is not in the
    <a>resolved</a> state, reject |promise| with an {{InvalidStateError}} and
    abort these steps.
1.  Reconfigure the UA's internal implementation of {{Navigator/usb}} in the
    <a>global object</a> associated with <var ignore>context</var> so that it is
    <a>controlled by</a> |test|.
1.  <a>Resolve</a> |promise|.

When invoked, the {{USBTest/addFakeDevice(deviceInit)}} method MUST run these
steps:

1.  Let |test| be the {{USBTest}} instance on which this method was invoked.
1.  If |test|.{{USBTest/[[initializationPromise]]}} is not in the
    <a>resolved</a> state, raise an {{InvalidStateError}} and abort these steps.
1.  Let |fakeDevice| be a new {{FakeUSBDevice}}.
1.  <a>Queue a task</a> to, for each {{USB}} instance <a>controlled by</a>
    |test|, perform the steps described in [[!WebUSB]] for handling a new device
    that is connected to the system.
1.  Return |fakeDevice|.

When invoked, the {{USBTest/reset()}} method MUST return a new {{Promise}}
|promise| and run the following steps <a>in parallel</a>:

1.  Let |test| be the {{USBTest}} instance on which this method was invoked.
1.  For each {{FakeUSBDevice}} |fakeDevice| previously returned by
    {{USBTest/addFakeDevice()}}, invoke
    |fakeDevice|.{{FakeUSBDevice/disconnect()}}.
1.  <a>Resolve</a> |promise|.

{{USBDeviceRequestEvent}} instances have an <a>internal slot</a>
<dfn attribute for="USBDeviceRequestEvent">\[[promise]]</dfn> that holds a
{{Promise}}.

When invoked, the {{USBDeviceRequestEvent/respondWith(result)}} method MUST run
the following steps <a>in parallel</a>:

1.  Wait until |result| settles.
1.  Let |event| be the {{USBDeviceRequestEvent}} instance on which this method
    was invoked.
1.  If |result| resolved with |response| and |response| is an instance of
    {{FakeUSBDevice}} resolve |event|@{{[[promise]]}} with |device|.
1.  Otherwise, reject |event|@{{[[promise]]}} with a {{NotFoundError}}.

## {{USB}} Behavior ## {#usb-behavior}

When {{USB/requestDevice(options)}} is invoked on a {{USB}} instance
<a>controlled by</a> a {{USBTest}} |test| the UA MUST return a new {{Promise}}
|promise| and perform the following steps <a>in parallel</a>:

1.  Let |event| be a new {{USBDeviceRequestEvent}}.
1.  Set |event|.|filters| to a new {{FrozenArray}}.
1.  Copy the members of
    <var ignore>options</var>.{{USBDeviceRequestOptions/filters}} into
    |event|.|filters|.
1.  Set |event|@{{[[promise]]}} to |promise|.
1.  <a>Fire an event</a> named <a>requestdevice</a> on |test|, using |event|
    as the event object.

## Events ## {#events}

<dfn>requestdevice</dfn>: Fired on a {{USBTest}} object when
{{USB/requestDevice(options)}} is invoked after {{USBTest/initialize()}} has
been called.

# Fake Devices # {#fake-devices}

To permit testing without physical hardware this specification defines a method
for tests to add simulated USB devices by calling {{USBTest/addFakeDevice()}}
with an instance of {{FakeUSBDeviceInit}} containing the properties of the
device to be added.

<xmp class="idl">
  interface FakeUSBDevice : EventTarget {
    attribute EventHandler onclose;

    void disconnect();
  };

  dictionary FakeUSBDeviceInit {
    required octet usbVersionMajor;
    required octet usbVersionMinor;
    required octet usbVersionSubminor;
    required octet deviceClass;
    required octet deviceSubclass;
    required octet deviceProtocol;
    required unsigned short vendorId;
    required unsigned short productId;
    required octet deviceVersionMajor;
    required octet deviceVersionMinor;
    required octet deviceVersionSubminor;
    DOMString? manufacturerName;
    DOMString? productName;
    DOMString? serialNumber;
    octet activeConfigurationValue = 0;
    sequence<FakeUSBConfigurationInit> configurations;
  };

  dictionary FakeUSBConfigurationInit {
    required octet configurationValue;
    DOMString? configurationName;
    sequence<FakeUSBInterfaceInit> interfaces;
  };

  dictionary FakeUSBInterfaceInit {
    required octet interfaceNumber;
    sequence<FakeUSBAlternateInterfaceInit> alternates;
  };

  dictionary FakeUSBAlternateInterfaceInit {
    required octet alternateSetting;
    required octet interfaceClass;
    required octet interfaceSubclass;
    required octet interfaceProtocol;
    DOMString? interfaceName;
    sequence<FakeUSBEndpointInit> endpoints;
  };

  dictionary FakeUSBEndpointInit {
    required octet endpointNumber;
    required USBDirection direction;
    required USBEndpointType type;
    required unsigned long packetSize;
  };
</xmp>

When a {{USBDevice}} |device| is initialized from an fake USB device described
by a {{FakeUSBDeviceInit}} |init| passed to {{USBTest/addFakeDevice()}} the
attributes of |device| SHALL be initialized as follows and |device| will
<dfn local-lt="corresponding to">correspond to</dfn> the {{FakeUSBDevice}}
returned by {{USBTest/addFakeDevice()}}:

1.  For each non-sequence attribute of |init| other than
    {{FakeUSBDeviceInit/activeConfigurationValue}} the attribute of |device|
    with the same name SHALL be set to its value.
1.  For each sequence of {{FakeUSBConfigurationInit}}, {{FakeUSBInterfaceInit}}
    {{FakeUSBAlternateInterfaceInit}} and {{FakeUSBEndpointInit}} objects
    corresponding {{USBConfiguration}}, {{USBInterface}},
    {{USBAlternateInterface}} and {{USBEndpoint}} objects SHALL be created by
    similarly copying attributes with the same names and be used to build an
    identical hierarchy of objects in the {{USBDevice/configurations}},
    {{USBConfiguration/interfaces}}, {{USBInterface/alternates}} and
    {{USBAlternateInterface/endpoints}} {{FrozenArray}}s.
1.  If a {{USBConfiguration}} instance |config| with
    {{USBConfiguration/configurationValue}} equal to
    |init|.{{FakeUSBDeviceInit/activeConfigurationValue}} then
    |device|.{{USBDevice/configuration}} SHALL be set to |config|, otherwise
    <code>null</code>.

When invoked, the {{FakeUSBDevice/disconnect()}} method MUST,
<a>queue a task</a> to, for each {{USB}} instance <a>controlled by</a> the
{{USBTest}} instance from which target of this invocation was returned, perform
the steps described in [[!WebUSB]] for handling the removal of a device that was
connected to the system.

## {{USBDevice}} Behavior ## {#usbdevice-behavior}

When the {{USBDevice/open()}}, {{USBDevice/close()}},
{{USBDevice/selectConfiguration()}}, {{USBDevice/claimInterface()}},
{{USBDevice/releaseInterface()}}, {{USBDevice/selectAlternateInterface()}},
{{USBDevice/clearHalt()}} and {{USBDevice/reset()}} methods are invoked on a
{{USBDevice}} |device| <a>corresponding to</a> a {{FakeUSBDevice}} the UA MUST
behave as though |device| contains the configurations, interfaces and endpoints
described in the {{FakeUSBDeviceInit}} from which |device| was initialized exist
and can be claimed by the caller.

When {{USBDevice/close()}} is invoked on a {{USBDevice}} |device| the UA MUST
<a>fire an event</a> named <code>close</code> at the {{FakeUSBDevice}}
instance <a>corresponding to</a> |device|, if one exists.

When {{USBDevice/controlTransferIn(setup, length)}} is invoked on a
{{USBDevice}} |device| <a>corresponding to</a> a {{FakeUSBDevice}} the UA MUST,
assuming all other pre-conditions for the operation are satisfied, behave as
though the device responded with a packet containing the bytes
<code>[|length| >> 8, |length| & 0xFF,
|setup|.{{USBControlTransferParameters/request}},
|setup|.{{USBControlTransferParameters/value}} >> 8,
|setup|.{{USBControlTransferParameters/value}} & 0xFF,
|setup|.{{USBControlTransferParameters/index}} >> 8,
|setup|.{{USBControlTransferParameters/index}} & 0xFF]</code>, truncated to
|length| bytes.

When {{USBDevice/controlTransferOut(setup, data)}} is invoked on a {{USBDevice}}
|device| <a>corresponding to</a> a {{FakeUSBDevice}} the UA MUST, assuming all
other pre-conditions for the operation are satisfied, behave as though the
transfer succeeded in sending <var ignore>data</var>.<code>length</code> bytes.

When {{USBDevice/transferIn(endpointNumber, length)}} is invoked on a
{{USBDevice}} |device| <a>corresponding to</a> a {{FakeUSBDevice}} the UA MUST,
assuming all other pre-conditions for the operation are satisfied, behave as
though the device responded with |length| bytes of data consisting of the
values <code>0</code> through <code>255</code> repeated as necessary.

When {{USBDevice/transferOut(endpointNumber, data)}} is invoked on a
{{USBDevice}} |device| <a>corresponding to</a> a {{FakeUSBDevice}} the UA MUST,
assuming all other pre-conditions for the operation are satisfied, behave as
though the transfer succeeded in sending
<var ignore>data</var>.<code>length</code> bytes.

When {{USBDevice/isochronousTransferIn(endpointNumber, packetLengths)}} is
invoked on a {{USBDevice}} |device| <a>corresponding to</a> a {{FakeUSBDevice}}
the UA MUST, assuming all other pre-conditions for the operation are satisfied,
behave as though the device responded with
|packetLengths|.<code>length</code> packets, each containing
|packetLengths|<code>[i]</code> bytes of data consisting of the values
<code>0</code> through <code>255</code> repeated as necessary.

When {{USBDevice/isochronousTransferOut(endpointNumber, data, packetLengths)}}
is invoked on a {{USBDevice}} |device| <a>corresponding to</a> a
{{FakeUSBDevice}} the UA MUST, assuming all other pre-conditions for the
operation are satisfied, behave as though the transfer succeeded in sending
|packetLengths|.<code>length</code> packets, each containing
|packetLengths|<code>[i]</code> bytes.

<pre class="anchors">
spec: ECMAScript; urlPrefix: https://tc39.github.io/ecma262/#
    type: dfn
        text: internal slot; url: sec-object-internal-methods-and-internal-slots
</pre>

<pre class="link-defaults">
spec:html; type:dfn; for:/; text:global object
spec:webidl; type:dfn; text:resolve
</pre>
